import { useMemo } from 'react';

import { contextSrv as ctx } from 'app/core/services/context_srv';
import { PERMISSIONS_CONTACT_POINTS_READ } from 'app/features/alerting/unified/components/contact-points/permissions';
import {
  PERMISSIONS_TIME_INTERVALS_MODIFY,
  PERMISSIONS_TIME_INTERVALS_READ,
} from 'app/features/alerting/unified/components/mute-timings/permissions';
import {
  PERMISSIONS_NOTIFICATION_POLICIES_MODIFY,
  PERMISSIONS_NOTIFICATION_POLICIES_READ,
} from 'app/features/alerting/unified/components/notification-policies/permissions';
import { useFolder } from 'app/features/alerting/unified/hooks/useFolder';
import { AlertmanagerChoice } from 'app/plugins/datasource/alertmanager/types';
import { AccessControlAction } from 'app/types';
import { CombinedRule, RuleGroupIdentifierV2 } from 'app/types/unified-alerting';
import { RulerRuleDTO } from 'app/types/unified-alerting-dto';

import { alertmanagerApi } from '../api/alertmanagerApi';
import { useAlertmanager } from '../state/AlertmanagerContext';
import { getInstancesPermissions, getNotificationsPermissions, getRulesPermissions } from '../utils/access-control';
import { getRulesSourceName } from '../utils/datasource';
import { getGroupOriginName } from '../utils/groupIdentifier';
import { isAdmin } from '../utils/misc';
import { isFederatedRuleGroup, isPluginProvidedRule, rulerRuleType } from '../utils/rules';

import { useIsRuleEditable } from './useIsRuleEditable';

/**
 * These hooks will determine if
 *  1. the action is supported in the current context (alertmanager, alert rule or general context)
 *  2. user is allowed to perform actions based on their set of permissions / assigned role
 */

// this enum lists all of the available actions we can perform within the context of an alertmanager
export enum AlertmanagerAction {
  // configuration
  ViewExternalConfiguration = 'view-external-configuration',
  UpdateExternalConfiguration = 'update-external-configuration',

  // contact points
  CreateContactPoint = 'create-contact-point',
  ViewContactPoint = 'view-contact-point',
  UpdateContactPoint = 'edit-contact-points',
  DeleteContactPoint = 'delete-contact-point',
  ExportContactPoint = 'export-contact-point',

  // notification templates
  CreateNotificationTemplate = 'create-notification-template',
  ViewNotificationTemplate = 'view-notification-template',
  UpdateNotificationTemplate = 'edit-notification-template',
  DeleteNotificationTemplate = 'delete-notification-template',
  DecryptSecrets = 'decrypt-secrets',

  // notification policies
  CreateNotificationPolicy = 'create-notification-policy',
  ViewNotificationPolicyTree = 'view-notification-policy-tree',
  UpdateNotificationPolicyTree = 'update-notification-policy-tree',
  DeleteNotificationPolicy = 'delete-notification-policy',
  ExportNotificationPolicies = 'export-notification-policies',
  ViewAutogeneratedPolicyTree = 'view-autogenerated-policy-tree',

  // silences – these cannot be deleted only "expired" (updated)
  CreateSilence = 'create-silence',
  ViewSilence = 'view-silence',
  UpdateSilence = 'update-silence',
  PreviewSilencedInstances = 'preview-silenced-alerts',

  // mute timings
  ViewMuteTiming = 'view-mute-timing',
  CreateMuteTiming = 'create-mute-timing',
  UpdateMuteTiming = 'update-mute-timing',
  DeleteMuteTiming = 'delete-mute-timing',
  ExportMuteTimings = 'export-mute-timings',

  // Alert groups
  ViewAlertGroups = 'view-alert-groups',
}

// this enum lists all of the available actions we can take on a single alert rule
export enum AlertRuleAction {
  Duplicate = 'duplicate-alert-rule',
  View = 'view-alert-rule',
  Update = 'update-alert-rule',
  Delete = 'delete-alert-rule',
  Explore = 'explore-alert-rule',
  Silence = 'silence-alert-rule',
  ModifyExport = 'modify-export-rule',
  Pause = 'pause-alert-rule',
  Restore = 'restore-alert-rule',
  DeletePermanently = 'delete-alert-rule-permanently',
}

// this enum lists all of the actions we can perform within alerting in general, not linked to a specific
// alert source, rule or alertmanager
export enum AlertingAction {
  // internal (Grafana managed)
  CreateAlertRule = 'create-alert-rule',
  ViewAlertRule = 'view-alert-rule',
  UpdateAlertRule = 'update-alert-rule',
  DeleteAlertRule = 'delete-alert-rule',
  ExportGrafanaManagedRules = 'export-grafana-managed-rules',
  ReadConfigurationStatus = 'read-configuration-status',

  // external (any compatible alerting data source)
  CreateExternalAlertRule = 'create-external-alert-rule',
  ViewExternalAlertRule = 'view-external-alert-rule',
  UpdateExternalAlertRule = 'update-external-alert-rule',
  DeleteExternalAlertRule = 'delete-external-alert-rule',
}

// these just makes it easier to read the code :)
const AlwaysSupported = true;
const NotSupported = false;

export type Action = AlertmanagerAction | AlertingAction | AlertRuleAction;
export type Ability = [actionSupported: boolean, actionAllowed: boolean];
export type Abilities<T extends Action> = Record<T, Ability>;

/**
 * This one will check for alerting abilities that don't apply to any particular alert source or alert rule
 */
export const useAlertingAbilities = (): Abilities<AlertingAction> => {
  return {
    // internal (Grafana managed)
    [AlertingAction.CreateAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleCreate),
    [AlertingAction.ViewAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleRead),
    [AlertingAction.UpdateAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleUpdate),
    [AlertingAction.DeleteAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleDelete),
    [AlertingAction.ExportGrafanaManagedRules]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleRead),
    [AlertingAction.ReadConfigurationStatus]: [
      AlwaysSupported,
      ctx.hasPermission(AccessControlAction.AlertingInstanceRead) ||
        ctx.hasPermission(AccessControlAction.AlertingNotificationsRead),
    ],

    // external
    [AlertingAction.CreateExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite),
    [AlertingAction.ViewExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalRead),
    [AlertingAction.UpdateExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite),
    [AlertingAction.DeleteExternalAlertRule]: toAbility(AlwaysSupported, AccessControlAction.AlertingRuleExternalWrite),
  };
};

export const useAlertingAbility = (action: AlertingAction): Ability => {
  const allAbilities = useAlertingAbilities();
  return allAbilities[action];
};

/**
 * This hook will check if we support the action and have sufficient permissions for it on a single alert rule
 */
export function useAlertRuleAbility(rule: CombinedRule, action: AlertRuleAction): Ability {
  const abilities = useAllAlertRuleAbilities(rule);

  return useMemo(() => {
    return abilities[action];
  }, [abilities, action]);
}

export function useAlertRuleAbilities(rule: CombinedRule, actions: AlertRuleAction[]): Ability[] {
  const abilities = useAllAlertRuleAbilities(rule);

  return useMemo(() => {
    return actions.map((action) => abilities[action]);
  }, [abilities, actions]);
}

export function useRulerRuleAbility(
  rule: RulerRuleDTO | undefined,
  groupIdentifier: RuleGroupIdentifierV2,
  action: AlertRuleAction
): Ability {
  const abilities = useAllRulerRuleAbilities(rule, groupIdentifier);

  return useMemo(() => {
    return abilities[action];
  }, [abilities, action]);
}

export function useRulerRuleAbilities(
  rule: RulerRuleDTO,
  groupIdentifier: RuleGroupIdentifierV2,
  actions: AlertRuleAction[]
): Ability[] {
  const abilities = useAllRulerRuleAbilities(rule, groupIdentifier);

  return useMemo(() => {
    return actions.map((action) => abilities[action]);
  }, [abilities, actions]);
}

// This hook is being called a lot in different places
// In some cases multiple times for ~80 rules (e.g. on the list page)
// We need to investigate further if some of these calls are redundant
// In the meantime, memoizing the result helps
export function useAllAlertRuleAbilities(rule: CombinedRule): Abilities<AlertRuleAction> {
  const rulesSourceName = getRulesSourceName(rule.namespace.rulesSource);

  const {
    isEditable,
    isRemovable,
    isRulerAvailable = false,
    loading,
  } = useIsRuleEditable(rulesSourceName, rule.rulerRule);
  const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
  const canSilence = useCanSilence(rule.rulerRule);

  const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
    const isProvisioned =
      rulerRuleType.grafana.rule(rule.rulerRule) && Boolean(rule.rulerRule.grafana_alert.provenance);
    const isFederated = isFederatedRuleGroup(rule.group);
    const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule.rulerRule);
    const isPluginProvided = isPluginProvidedRule(rule.rulerRule);

    // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
    const immutableRule = isProvisioned || isFederated || isPluginProvided;

    // while we gather info, pretend it's not supported
    const MaybeSupported = loading ? NotSupported : isRulerAvailable;
    const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;

    // Creating duplicates of plugin-provided rules does not seem to make a lot of sense
    const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;

    const rulesPermissions = getRulesPermissions(rulesSourceName);

    const abilities: Abilities<AlertRuleAction> = {
      [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
      [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read),
      [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
      [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
      [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
      [AlertRuleAction.Silence]: canSilence,
      [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed],
      [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
      [AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
      [AlertRuleAction.DeletePermanently]: [
        MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule,
        (isRemovable && isAdmin()) ?? false,
      ],
    };

    return abilities;
  }, [rule, loading, isRulerAvailable, isEditable, isRemovable, rulesSourceName, exportAllowed, canSilence]);

  return abilities;
}

export function useAllRulerRuleAbilities(
  rule: RulerRuleDTO | undefined,
  groupIdentifier: RuleGroupIdentifierV2
): Abilities<AlertRuleAction> {
  const rulesSourceName = getGroupOriginName(groupIdentifier);

  const { isEditable, isRemovable, isRulerAvailable = false, loading } = useIsRuleEditable(rulesSourceName, rule);
  const [_, exportAllowed] = useAlertingAbility(AlertingAction.ExportGrafanaManagedRules);
  const canSilence = useCanSilence(rule);

  const abilities = useMemo<Abilities<AlertRuleAction>>(() => {
    const isProvisioned = rulerRuleType.grafana.rule(rule) && Boolean(rule.grafana_alert.provenance);
    // const isFederated = isFederatedRuleGroup();
    const isFederated = false;
    const isGrafanaManagedAlertRule = rulerRuleType.grafana.rule(rule);
    const isPluginProvided = isPluginProvidedRule(rule);

    // if a rule is either provisioned, federated or provided by a plugin rule, we don't allow it to be removed or edited
    const immutableRule = isProvisioned || isFederated || isPluginProvided;

    // while we gather info, pretend it's not supported
    const MaybeSupported = loading ? NotSupported : isRulerAvailable;
    const MaybeSupportedUnlessImmutable = immutableRule ? NotSupported : MaybeSupported;

    // Creating duplicates of plugin-provided rules does not seem to make a lot of sense
    const duplicateSupported = isPluginProvided ? NotSupported : MaybeSupported;

    const rulesPermissions = getRulesPermissions(rulesSourceName);

    const abilities: Abilities<AlertRuleAction> = {
      [AlertRuleAction.Duplicate]: toAbility(duplicateSupported, rulesPermissions.create),
      [AlertRuleAction.View]: toAbility(AlwaysSupported, rulesPermissions.read),
      [AlertRuleAction.Update]: [MaybeSupportedUnlessImmutable, isEditable ?? false],
      [AlertRuleAction.Delete]: [MaybeSupportedUnlessImmutable, isRemovable ?? false],
      [AlertRuleAction.Explore]: toAbility(AlwaysSupported, AccessControlAction.DataSourcesExplore),
      [AlertRuleAction.Silence]: canSilence,
      [AlertRuleAction.ModifyExport]: [isGrafanaManagedAlertRule, exportAllowed],
      [AlertRuleAction.Pause]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
      [AlertRuleAction.Restore]: [MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule, isEditable ?? false],
      [AlertRuleAction.DeletePermanently]: [
        MaybeSupportedUnlessImmutable && isGrafanaManagedAlertRule,
        (isRemovable && isAdmin()) ?? false,
      ],
    };

    return abilities;
  }, [rule, loading, isRulerAvailable, rulesSourceName, isEditable, isRemovable, canSilence, exportAllowed]);

  return abilities;
}

export function useAllAlertmanagerAbilities(): Abilities<AlertmanagerAction> {
  const {
    selectedAlertmanager,
    hasConfigurationAPI,
    isGrafanaAlertmanager: isGrafanaFlavoredAlertmanager,
  } = useAlertmanager();

  // These are used for interacting with Alertmanager resources where we apply alert.notifications:<name> permissions.
  // There are different permissions based on wether the built-in alertmanager is selected (grafana) or an external one.
  const notificationsPermissions = getNotificationsPermissions(selectedAlertmanager!);
  const instancePermissions = getInstancesPermissions(selectedAlertmanager!);

  // list out all of the abilities, and if the user has permissions to perform them
  const abilities: Abilities<AlertmanagerAction> = {
    // -- configuration --
    [AlertmanagerAction.ViewExternalConfiguration]: toAbility(
      AlwaysSupported,
      AccessControlAction.AlertingNotificationsExternalRead
    ),
    [AlertmanagerAction.UpdateExternalConfiguration]: toAbility(
      hasConfigurationAPI,
      AccessControlAction.AlertingNotificationsExternalWrite
    ),
    // -- contact points --
    [AlertmanagerAction.CreateContactPoint]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.create,
      // TODO: Move this into the permissions config and generalise that code to allow for an array of permissions
      ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversCreate] : [])
    ),
    [AlertmanagerAction.ViewContactPoint]: toAbility(
      AlwaysSupported,
      notificationsPermissions.read,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_CONTACT_POINTS_READ : [])
    ),
    [AlertmanagerAction.UpdateContactPoint]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.update,
      ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : [])
    ),
    [AlertmanagerAction.DeleteContactPoint]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.delete,
      ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingReceiversWrite] : [])
    ),
    // At the time of writing, only Grafana flavored alertmanager supports exporting,
    // and if a user can view the contact point, then they can also export it
    // So the only check we make is if the alertmanager is Grafana flavored
    [AlertmanagerAction.ExportContactPoint]: [isGrafanaFlavoredAlertmanager, isGrafanaFlavoredAlertmanager],
    // -- notification templates --
    [AlertmanagerAction.CreateNotificationTemplate]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.create,
      ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : [])
    ),
    [AlertmanagerAction.ViewNotificationTemplate]: toAbility(
      AlwaysSupported,
      notificationsPermissions.read,
      ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesRead] : [])
    ),
    [AlertmanagerAction.UpdateNotificationTemplate]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.update,
      ...(isGrafanaFlavoredAlertmanager ? [AccessControlAction.AlertingTemplatesWrite] : [])
    ),
    [AlertmanagerAction.DeleteNotificationTemplate]: toAbility(hasConfigurationAPI, notificationsPermissions.delete),
    // -- notification policies --
    [AlertmanagerAction.CreateNotificationPolicy]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.create,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_MODIFY : [])
    ),
    [AlertmanagerAction.ViewNotificationPolicyTree]: toAbility(
      AlwaysSupported,
      notificationsPermissions.read,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_READ : [])
    ),
    [AlertmanagerAction.UpdateNotificationPolicyTree]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.update,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_MODIFY : [])
    ),
    [AlertmanagerAction.DeleteNotificationPolicy]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.delete,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_NOTIFICATION_POLICIES_MODIFY : [])
    ),
    [AlertmanagerAction.ExportNotificationPolicies]: toAbility(
      isGrafanaFlavoredAlertmanager,
      notificationsPermissions.read
    ),
    [AlertmanagerAction.DecryptSecrets]: toAbility(
      isGrafanaFlavoredAlertmanager,
      notificationsPermissions.provisioning.readSecrets
    ),
    [AlertmanagerAction.ViewAutogeneratedPolicyTree]: [isGrafanaFlavoredAlertmanager, isAdmin()],
    // -- silences --
    // for now, all supported Alertmanager flavors have API endpoints for managing silences
    [AlertmanagerAction.CreateSilence]: toAbility(AlwaysSupported, instancePermissions.create),
    [AlertmanagerAction.ViewSilence]: toAbility(AlwaysSupported, instancePermissions.read),
    [AlertmanagerAction.UpdateSilence]: toAbility(AlwaysSupported, instancePermissions.update),
    [AlertmanagerAction.PreviewSilencedInstances]: toAbility(AlwaysSupported, instancePermissions.read),
    // -- mute timings --
    [AlertmanagerAction.CreateMuteTiming]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.create,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
    ),
    [AlertmanagerAction.ViewMuteTiming]: toAbility(
      AlwaysSupported,
      notificationsPermissions.read,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_READ : [])
    ),
    [AlertmanagerAction.UpdateMuteTiming]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.update,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
    ),
    [AlertmanagerAction.DeleteMuteTiming]: toAbility(
      hasConfigurationAPI,
      notificationsPermissions.delete,
      ...(isGrafanaFlavoredAlertmanager ? PERMISSIONS_TIME_INTERVALS_MODIFY : [])
    ),
    [AlertmanagerAction.ExportMuteTimings]: toAbility(isGrafanaFlavoredAlertmanager, notificationsPermissions.read),
    [AlertmanagerAction.ViewAlertGroups]: toAbility(AlwaysSupported, instancePermissions.read),
  };

  return abilities;
}

export function useAlertmanagerAbility(action: AlertmanagerAction): Ability {
  const abilities = useAllAlertmanagerAbilities();

  return useMemo(() => {
    return abilities[action];
  }, [abilities, action]);
}

export function useAlertmanagerAbilities(actions: AlertmanagerAction[]): Ability[] {
  const abilities = useAllAlertmanagerAbilities();

  return useMemo(() => {
    return actions.map((action) => abilities[action]);
  }, [abilities, actions]);
}

const { useGetGrafanaAlertingConfigurationStatusQuery } = alertmanagerApi;
/**
 * We don't want to show the silence button if either
 * 1. the user has no permissions to create silences
 * 2. the admin has configured to only send instances to external AMs
 */
function useCanSilence(rule?: RulerRuleDTO): [boolean, boolean] {
  const folderUID = rulerRuleType.grafana.rule(rule) ? rule.grafana_alert.namespace_uid : undefined;
  const { loading: folderIsLoading, folder } = useFolder(folderUID);

  const isGrafanaManagedRule = rule && rulerRuleType.grafana.rule(rule);
  const isGrafanaRecording = rulerRuleType.grafana.recordingRule(rule);

  const silenceSupported = useGrafanaRulesSilenceSupport();
  const canSilenceInFolder = useCanSilenceInFolder(folderUID);

  if (!rule) {
    return [false, false];
  }

  // we don't support silencing when the rule is not a Grafana managed alerting rule
  // we simply don't know what Alertmanager the ruler is sending alerts to
  if (!isGrafanaManagedRule || isGrafanaRecording || folderIsLoading || !folder) {
    return [false, false];
  }

  return [silenceSupported, canSilenceInFolder];
}

function useCanSilenceInFolder(folderUID?: string) {
  const folderPermissions = useFolderPermissions(folderUID);

  const hasFolderSilencePermission = folderPermissions[AccessControlAction.AlertingSilenceCreate] ?? false;
  const hasGlobalSilencePermission = ctx.hasPermission(AccessControlAction.AlertingInstanceCreate);

  // User is permitted to silence if they either have the "global" permissions of "AlertingInstanceCreate",
  // or the folder specific access control of "AlertingSilenceCreate"
  const allowedToSilence = hasGlobalSilencePermission || hasFolderSilencePermission;
  return allowedToSilence;
}

function useGrafanaRulesSilenceSupport() {
  const { currentData: amConfigStatus, isLoading } = useGetGrafanaAlertingConfigurationStatusQuery(undefined);

  const interactsOnlyWithExternalAMs = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.External;
  const interactsWithAll = amConfigStatus?.alertmanagersChoice === AlertmanagerChoice.All;
  const silenceSupported = !interactsOnlyWithExternalAMs || interactsWithAll;

  return isLoading ? false : silenceSupported;
}

function useFolderPermissions(folderUID?: string): Record<string, boolean> {
  const { folder } = useFolder(folderUID);
  return folder?.accessControl ?? {};
}

// just a convenient function
const toAbility = (
  supported: boolean,
  /** If user has any of these permissions, then they are allowed to perform the action */
  ...actions: AccessControlAction[]
): Ability => [supported, actions.some((action) => action && ctx.hasPermission(action))];
